package cn.ming.maven.plugin;

import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.repository.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;

import javax.inject.Inject;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * <pre>
 * 通过数据库依赖配置校验项目依赖版本插件实现类
 * </pre>
 *
 * @author zhuming
 * @version 1.0.0
 * @since 2022-12-12 17:36
 */
@Mojo(name = "check", defaultPhase = LifecyclePhase.COMPILE)
public class CheckVersionMojo extends AbstractMojo {
    @Inject
    private MavenProject project;
    @Inject
    private RepositorySystem system;
    @Inject
    private MavenSession session;
    /**
     * The project's remote repositories to use for the resolution of project dependencies.
     */
    @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
    private List<RemoteRepository> projectRepos;
    /**
     * The entry point to Aether, i.e. the component doing all the work.
     */
    @Component
    private org.eclipse.aether.RepositorySystem repoSystem;
    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
    private RepositorySystemSession repoSession;


    /**
     * 数据库连接参数
     */
    @Parameter(name = "dbDriver", required = true)
    private String dbDriver;
    @Parameter(name = "dbUrl", required = true)
    private String dbUrl;
    @Parameter(name = "dbUsername", required = true)
    private String dbUsername;
    @Parameter(name = "dbPassword", required = true)
    private String dbPassword;

    /**
     * 数据库表和字段配置
     */
    @Parameter(name = "checkTableName", required = true)
    private String checkTableName;
    @Parameter(name = "artifactIdColumn", required = true)
    private String artifactIdColumn;
    @Parameter(name = "startBaseVersionColumn", required = true)
    private String startBaseVersionColumn;
    @Parameter(name = "endBaseVersionColumn", required = true)
    private String endBaseVersionColumn;
    @Parameter(name = "excludeBaseVersionsColumn", required = true)
    private String excludeBaseVersionsColumn;

    @Parameter(name = "exportPath")
    private String exportPath;

    /**
     * 核心实现
     */
    @Override
    public void execute() throws MojoFailureException {
        //1、读取项目引用的依赖
        Map<String, List<String>> artifactMap = this.getArtifactMap();
        if (artifactMap.isEmpty()) {
            throw new MojoFailureException("未读取到项目引用的依赖信息，请确认！");
        }
        Map<String, String> artifactMaxVersionMap = Maps.newHashMapWithExpectedSize(artifactMap.size());
        //提取最大版本号
        for (Map.Entry<String, List<String>> entry : artifactMap.entrySet()) {
            artifactMaxVersionMap.put(entry.getKey(), entry.getValue().stream().max(String::compareToIgnoreCase).get());
        }
        //2、连接数据库
        Connection connection = this.getConnection();
        //3、获取数据库配置依赖版本信息
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT ");
        sb.append(artifactIdColumn.toUpperCase());
        sb.append(",");
        sb.append(startBaseVersionColumn.toUpperCase());
        sb.append(",");
        sb.append(endBaseVersionColumn.toUpperCase());
        sb.append(",");
        sb.append(excludeBaseVersionsColumn.toUpperCase());
        sb.append(" FROM ");
        sb.append(checkTableName.toUpperCase());
        sb.append(" WHERE ");
        sb.append(artifactIdColumn.toUpperCase());
        sb.append(" IN(");
        sb.append(artifactMaxVersionMap.keySet().stream().map(artifactId -> "'" + artifactId + "'").collect(Collectors.joining(",")));
        sb.append(")");
        List<DependenciesVersion> dependenciesVersionList = Lists.newArrayList();
        PreparedStatement pst = null;
        try {
            getLog().info(String.format("查询依赖版本配置SQL：%s", sb));
            pst = connection.prepareStatement(sb.toString());
            ResultSet rs = pst.executeQuery();
            while (rs.next()) {
                DependenciesVersion dependenciesVersion = new DependenciesVersion();
                dependenciesVersion.setArtifactId(rs.getString(artifactIdColumn));
                dependenciesVersion.setStartBaseVersion(rs.getString(startBaseVersionColumn));
                dependenciesVersion.setEndBaseVersion(rs.getString(endBaseVersionColumn));
                dependenciesVersion.setExcludeBaseVersions(rs.getString(excludeBaseVersionsColumn));
                dependenciesVersionList.add(dependenciesVersion);
            }
        } catch (Exception e) {
            throw new MojoFailureException(String.format("从数据库读取依赖版本校验数据失败！%s", e.getMessage()));
        } finally {
            if (Objects.nonNull(pst)) {
                try {
                    pst.close();
                } catch (SQLException e) {
                    getLog().error(String.format("关闭依赖版本校验数据库预编译对象失败！%s", e.getMessage()));
                }
            }
            try {
                connection.close();
            } catch (SQLException e) {
                getLog().error(String.format("关闭依赖版本校验数据库连接失败！%s", e.getMessage()));
            }
        }
        //校验依赖版本号
        this.checkVersion(dependenciesVersionList, artifactMaxVersionMap);
    }

    /**
     * 读取项目引用的依赖
     *
     * @return 项目引用的依赖列表
     */
    private Map<String, List<String>> getArtifactMap() throws MojoFailureException {
        Map<String, List<String>> artifactMap = Maps.newHashMap();
        for (Dependency dependency : project.getDependencies()) {
            Artifact artifact = system.createDependencyArtifact(dependency);
            List<String> artifactVersions = artifactMap.getOrDefault(artifact.getArtifactId(), Lists.newArrayList());
            artifactVersions.add(artifact.getBaseVersion());
            artifactMap.put(artifact.getArtifactId(), artifactVersions);

            // 提取子依赖
            DefaultArtifact defaultArtifact = new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), artifact.getType(), artifact.getVersion());
            org.eclipse.aether.graph.Dependency rootDependency = new org.eclipse.aether.graph.Dependency(defaultArtifact, "");
            try {
                CollectRequest collectRequest = new CollectRequest();
                collectRequest.setRoot(rootDependency);
                collectRequest.setRepositories(projectRepos);
                DependencyNode rootNode = repoSystem.collectDependencies(repoSession, collectRequest).getRoot();
                DependencyRequest request = new DependencyRequest(rootNode, null);
                DependencyResult result = repoSystem.resolveDependencies(repoSession, request);
                for (DependencyNode childNode : result.getRoot().getChildren()) {
                    org.eclipse.aether.artifact.Artifact childArtifact = childNode.getArtifact();
                    List<String> childArtifactVersions = artifactMap.getOrDefault(childArtifact.getArtifactId(), Lists.newArrayList());
                    childArtifactVersions.add(childArtifact.getBaseVersion());
                    artifactMap.put(childArtifact.getArtifactId(), childArtifactVersions);
                }
            } catch (DependencyCollectionException | DependencyResolutionException e) {
                throw new MojoFailureException(String.format("could not resolve dependencies for artifact：%s ---> REASON：%s", defaultArtifact, e.getMessage()));
            }
        }
        return artifactMap;
    }

    /**
     * 获取数据库连接
     *
     * @return 数据库连接
     * @throws MojoFailureException 数据库连接异常
     */
    private Connection getConnection() throws MojoFailureException {
        try {
            Class.forName(dbDriver);
            return DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
        } catch (ClassNotFoundException e) {
            throw new MojoFailureException("插件目前仅支持Mysql、Oracle数据库连接！");
        } catch (SQLException e) {
            throw new MojoFailureException(String.format("连接数据库失败，请检查连接参数配置！%s", e.getMessage()));
        }
    }

    /**
     * 校验依赖版本号
     *
     * @param dependenciesVersionList 数据库配置依赖列表
     * @param artifactMap             项目依赖列表
     */
    private void checkVersion(List<DependenciesVersion> dependenciesVersionList, Map<String, String> artifactMap) throws MojoFailureException {
        AtomicBoolean result = new AtomicBoolean(true);
        Map<String, DependenciesVersion> dependenciesVersionMap = dependenciesVersionList.stream().
                collect(Collectors.toMap(DependenciesVersion::getArtifactId, dependenciesVersion -> dependenciesVersion));
        List<DependenciesVersion> exportList = Lists.newArrayList();
        artifactMap.forEach((artifactId, artifactVersion) -> {
            if (!dependenciesVersionMap.containsKey(artifactId)) {
                DependenciesVersion exportData = new DependenciesVersion();
                exportData.setArtifactId(artifactId);
                exportData.setBaseVersion(artifactVersion);
                exportList.add(exportData);
                getLog().error(String.format("依赖包[%s]未配置数据库版本控制，请先配置！", artifactId));
                result.set(false);
            } else {
                DependenciesVersion exportData = dependenciesVersionMap.get(artifactId);
                exportData.setBaseVersion(artifactVersion);
                exportList.add(exportData);
                DependenciesVersion dependenciesVersion = dependenciesVersionMap.get(artifactId);
                String remark = "[有效版本参考区间：" + dependenciesVersion.getStartBaseVersion() + " ≤ version ≤ " + dependenciesVersion.getEndBaseVersion() + "]";
                if (!StringUtils.isNumeric(artifactVersion.replaceAll("\\.", "")) ||
                        !StringUtils.isNumeric(dependenciesVersion.getStartBaseVersion().replaceAll("\\.", "")) ||
                        !StringUtils.isNumeric(dependenciesVersion.getEndBaseVersion().replaceAll("\\.", ""))) {
                    getLog().warn(String.format("依赖包[%s]当前版本号[%s]/对应数据库配置版本号[min:%s, max:%s]中包含字母，插件无法自动校验，请人工核验！%s", artifactId, artifactVersion,
                            dependenciesVersion.getStartBaseVersion(), dependenciesVersion.getEndBaseVersion(), remark));
                } else if (StringUtils.isNotBlank(dependenciesVersion.getStartBaseVersion()) && this.compareVersion(artifactVersion, dependenciesVersion.getStartBaseVersion()) < 0) {
                    getLog().error(String.format("依赖包[%s]版本不能低于[%s]，请升级依赖版本！%s", artifactId, dependenciesVersion.getStartBaseVersion(), remark));
                    result.set(false);
                } else if (StringUtils.isNotBlank(dependenciesVersion.getEndBaseVersion()) && this.compareVersion(artifactVersion, dependenciesVersion.getEndBaseVersion()) > 0) {
                    getLog().error(String.format("依赖包[%s]版本不能高于[%s]，请降低依赖版本！%s", artifactId, dependenciesVersion.getEndBaseVersion(), remark));
                    result.set(false);
                }
                String excludeBaseVersion = dependenciesVersion.getExcludeBaseVersions();
                if (StringUtils.isNotBlank(excludeBaseVersion)) {
                    List<String> excludeBaseVersionList = Arrays.stream(excludeBaseVersion.split(",")).collect(Collectors.toList());
                    if (excludeBaseVersionList.contains(artifactVersion)) {
                        getLog().error(String.format("依赖包[%s]当前版本[%s]禁止使用/存在漏洞，请变更依赖版本！%s", artifactId, artifactVersion, remark));
                        result.set(false);
                    }
                }
            }
        });
        if (StringUtils.isNotBlank(exportPath)) {
            getLog().info("正在导出项目依赖版本信息至路径");
            try {
                EasyExcelFactory.write(exportPath, DependenciesVersion.class).
                        //excelType(ExcelTypeEnum.XLSX).
                        charset(StandardCharsets.UTF_8).
                        sheet("依赖版本信息").doWrite(exportList);
                getLog().info(String.format("项目依赖版本信息导出成功，文件路径--->%s", exportPath));
            } catch (Exception e) {
                getLog().error(String.format("项目依赖版本信息导出失败！--->REASON：%s", e.getMessage()));
            }
        }
        if (!result.get()) {
            throw new MojoFailureException("依赖版本校验不通过，请根据错误提示进行调整！");
        }
    }

    /**
     * 比较版本号大小
     *
     * @param version1 版本号1
     * @param version2 版本号2
     * @return 比较结果
     */
    private int compareVersion(String version1, String version2) {
        if (version1.equals(version2)) {
            return 0;
        }
        String[] version1Array = version1.split("\\.");
        String[] version2Array = version2.split("\\.");
        int index = 0;
        //获取最小长度值
        int minLen = Math.min(version1Array.length, version2Array.length);
        int diff = 0;
        //循环判断每位的大小
        while (index < minLen && (diff = Integer.parseInt(version1Array[index]) - Integer.parseInt(version2Array[index])) == 0) {
            index++;
        }
        if (diff == 0) {
            //如果位数不一致，比较多余位数
            for (int i = index; i < version1Array.length; i++) {
                if (Integer.parseInt(version1Array[i]) > 0) {
                    return 1;
                }
            }

            for (int i = index; i < version2Array.length; i++) {
                if (Integer.parseInt(version2Array[i]) > 0) {
                    return -1;
                }
            }
            return 0;
        }
        return diff > 0 ? 1 : -1;
    }

}
